package yicr.untils.gis.geoHash;


import lombok.extern.slf4j.Slf4j;
import yicr.untils.NslUtilCsv;
import yicr.untils.NslUtilFilePath;
import yicr.untils.NslUtilString;
import yicr.untils.gis.NslUtilGis;
import yicr.untils.gis.Polygon.NslPolygon;
import yicr.untils.gis.domain.NslGpsPoint;

import java.awt.geom.Point2D;
import java.io.File;
import java.util.*;

/**
 * <h3>Title:</h3>
 * <p>
 *
 * <h3>Description</h3>
 * <p>
 *
 * <h3>Author</h3>
 * <p>
 * Lin Shao
 * <h3>Update Time</h3>
 * <p>2021-09-18 22:39
 *
 * @author Lin Shao
 * @version 1.0
 */

public class NslUtilGeoHash {

    //经度做用20位，纬度用20位
    public static int defaultBitLength=20;//经纬度单独编码长度
    public static final double MINLAT = -90;
    public static final double MAXLAT = 90;
    public static final double MINLNG = -180;
    public static final double MAXLNG = 180;

    public static final double DefaultMinLatitudeInterval=0;

    public static final double DefaultMinLongitudeInterval=0;


    //定义编码映射关系
    final static HashMap<Character, Integer> lookup = new HashMap<Character, Integer>();

    private final static char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
            '9', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p',
            'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };
    //初始化编码映射内容
    static {
        int i = 0;
        for (char c : digits)
            lookup.put(c, i++);
    }



    static public GeoHash create(){
        return new GeoHash();
    }

    static public GeoHash create(int bitNumber){
        return new GeoHash(bitNumber);
    }

    //根据经纬度和范围，获取对应的二进制
    public static BitSet getBits(Double lat,Integer numBits, double floor, double ceiling) {
        BitSet buffer = new BitSet(numBits);
        for (int i = 0; i < numBits; i++) {
            double mid = (floor + ceiling) / 2;
            if (lat >= mid) {
                buffer.set(i);
                floor = mid;
            } else {
                ceiling = mid;
            }
        }
        return buffer;
    }

    /**
     * 获取 纬度 bitset
     * @param lat
     * @param numBits
     * @return
     */
    public static BitSet getLatitudeBits(Double lat,Integer numBits){
        return getBits(lat,numBits,MINLAT,MAXLAT);
    }

    /**
     * 根据 经纬度编码 geoHash
     * @param longitude
     * @param latitude
     * @param numbits
     * @return
     */
    public static  String encode(double longitude,double latitude,int numbits){
        BitSet latbits = getLatitudeBits(latitude,numbits);
        BitSet lonbits = getLongitudeBits(longitude,numbits);
        StringBuilder buffer = new StringBuilder();
        for (int i = 0; i < numbits; i++) {
            buffer.append( (lonbits.get(i))?'1':'0');
            buffer.append( (latbits.get(i))?'1':'0');
        }

        //log.info("buffer.toString() = {}",buffer.toString());
        //log.info("buffer.toString().length = {}",buffer.toString().length());
        String code = base32(Long.parseLong(buffer.toString(), 2));
        //Log.i("okunu", "encode  lat = " + lat + "  lng = " + lon + "  code = " + code);

        return code;
    }

    public static String encode(double longitude, double latitude) {
        return encode(longitude,latitude,defaultBitLength);
    }

    /**
     * 根据 geoHash坐标系中的X,Y，返回 GeoHash码
     * @param longitudeX
     * @param latitudeY
     * @return
     */
    public static String encode(long longitudeX,long latitudeY,int numbits){
        StringBuilder stringBuilder=new StringBuilder();
        for(int i=0;i<numbits;i++){
            long baseLong=1L<<i;
            if((longitudeX & baseLong) == 0){
                stringBuilder.append('0');
            }else {
                stringBuilder.append('1');
            }

            if((latitudeY & baseLong) == 0){
                stringBuilder.append('0');
            }else {
                stringBuilder.append('1');
            }
        }

        String code = base32(Long.parseLong(stringBuilder.toString(), 2));
        return code;
    }

    //根据二进制和范围解码
    static public double decode(BitSet bs, double floor, double ceiling) {
        double mid = 0;
        for (int i=0; i<bs.length(); i++) {
            mid = (floor + ceiling) / 2;
            if (bs.get(i))
                floor = mid;
            else
                ceiling = mid;
        }
        return mid;
    }

    /**
     * 对编码后的字符串解码，获取 经度、纬度, 第一个元素是经度
     * @param geoHashString
     * @return
     */
    public static double[] decode(String geoHashString){
        return decode(geoHashString,defaultBitLength);
    }

    /**
     * 对编码后的字符串解码，获取 经度、纬度
     * @param geoHashString
     * @return [lat,long]
     */
    static  public  double[] decode(String geoHashString,int numbits) {
        StringBuilder buffer = new StringBuilder();
        for (char c : geoHashString.toCharArray()) {
            int i = lookup.get(c) + 32;
            buffer.append( Integer.toString(i, 2).substring(1) );
        }

        BitSet lonset = new BitSet();
        BitSet latset = new BitSet();

        //偶数位，经度
        int j =0;
        for (int i=0; i< numbits*2;i+=2) {
            boolean isSet = false;
            if ( i < buffer.length() )
                isSet = buffer.charAt(i) == '1';
            lonset.set(j++, isSet);
        }

        //奇数位，纬度
        j=0;
        for (int i=1; i< numbits*2;i+=2) {
            boolean isSet = false;
            if ( i < buffer.length() )
                isSet = buffer.charAt(i) == '1';
            latset.set(j++, isSet);
        }

        double lon = decode(lonset, -180, 180);
        double lat = decode(latset, -90, 90);

        return new double[] {lon, lat};
    }





    //将经纬度合并后的二进制进行指定的32位编码
    private static String base32(long i) {
        char[] buf = new char[65];
        int charPos = 64;
        boolean negative = (i < 0);
        if (!negative){
            i = -i;
        }
        while (i <= -32) {
            buf[charPos--] = digits[(int) (-(i % 32))];
            i /= 32;
        }
        buf[charPos] = digits[(int) (-i)];
        if (negative){
            buf[--charPos] = '-';
        }
        return new String(buf, charPos, (65 - charPos));
    }


    /**
     * 获取 纬度 bitset
     * @param lat
     * @param numBits
     * @return
     */
    public static BitSet getLongitudeBits(Double lat,Integer numBits){
        return getBits(lat,numBits,MINLNG,MAXLNG);
    }




    /**
     * 根据 numBits,获取 经度/纬度 的 上边界和下边界
     * @param lonLat
     * @param floor
     * @param ceiling
     * @param numBits
     * @return
     */
    static public  double[] getCeilingAndFloor(double lonLat,double floor, double ceiling,Integer numBits ){
        double[] ceilingAndFloor=new double[2];
        double mid=0;
        BitSet buffer = new BitSet(numBits);
        for (int i = 0; i < numBits; i++) {
            if(lonLat>=mid){
                buffer.set(i);
                floor=mid;
            }else{
                ceiling=mid;
            }
            mid=(ceiling+floor)/2;
        }
        ceilingAndFloor[0]=ceiling;
        ceilingAndFloor[1]=floor;
        return ceilingAndFloor;
    }


    static public double[] getCeilingAndFloorOfLongitude(double longitude,Integer numBits){
        return getCeilingAndFloor(longitude,MINLNG,MAXLNG,numBits);
    }

    static public double[] getCeilingAndFloorOfLatitude(double latitude,Integer numBits){
        return getCeilingAndFloor(latitude,MINLAT,MAXLAT,numBits);
    }

    /**
     * 获取 指定经度所在栅格位置的中心点经度
     * @param longitude
     * @param numBits
     * @return
     */
    public static double getMidLongitude(double longitude,int numBits){
        double[] points=getCeilingAndFloorOfLongitude(longitude,numBits);
        return (points[0]+points[1])/2;
    }

    /**
     * 获取 指定纬度所在栅格位置的中心点纬度
     * @param latitude
     * @param numBits
     * @return
     */
    public static double getMidLatitude(double latitude,int numBits){
        double[] points=getCeilingAndFloorOfLatitude(latitude,numBits);
        return (points[0]+points[1])/2;
    }



    /**
     * 返回制定经纬点所在 geohash 所在矩形的4个定点,顺序为wise clock
     * // 默认20bit 对应8个字符的geohash编码，精度为：纬度方向+/-0.019km，经度方向：38m
     */
    static public List<NslGpsPoint> getVertexListByLonLat(double longitude, double latitude){

        double[] ceilingAndFloorOfLon=getCeilingAndFloor(longitude,-180,180,defaultBitLength);
        double[] ceilingAndFloorOfLat=getCeilingAndFloor(latitude,-90,90,defaultBitLength);
        NslGpsPoint NslGpsPoint1=new NslGpsPoint("0",ceilingAndFloorOfLon[1],ceilingAndFloorOfLat[0]);
        NslGpsPoint NslGpsPoint2=new NslGpsPoint("1",ceilingAndFloorOfLon[0],ceilingAndFloorOfLat[0]);
        NslGpsPoint NslGpsPoint3=new NslGpsPoint("2",ceilingAndFloorOfLon[0],ceilingAndFloorOfLat[1]);
        NslGpsPoint NslGpsPoint4=new NslGpsPoint("3",ceilingAndFloorOfLon[1],ceilingAndFloorOfLat[1]);
        List<NslGpsPoint> list=new ArrayList<>(4);
        list.add(NslGpsPoint1);
        list.add(NslGpsPoint2);
        list.add(NslGpsPoint3);
        list.add(NslGpsPoint4);
        return list;
    }


    public static double getMinIntervalOfLongitude(int numBits){
       double minLongitudeInterval = MAXLNG - MINLNG;
        for (int i = 0; i < numBits; i++) {
            minLongitudeInterval /= 2.0;
        }
        return minLongitudeInterval;
    }

    public static double getMinIntervalOfLatitude(int numBits){
        double minLatInterval = MAXLAT - MINLAT;
        for (int i = 0; i < numBits; i++) {
            minLatInterval /= 2.0;
        }
        return minLatInterval;
    }

    /**
     * 获取 指定经纬度 四周的 geoHash
     * @param lat
     * @param lon
     * @param numBits
     * @return
     */
    static public ArrayList<String> getAroundGeoHash(double lat, double lon,int numBits){
        //Log.i("okunu", "getArroundGeoHash  lat = " + lat + "  lng = " + lon);
        ArrayList<String> list = new ArrayList<>();

        double upLat = lat + getMinIntervalOfLatitude(numBits);
        double downLat = lat - getMinIntervalOfLatitude(numBits);

        double leftLng = lon - getMinIntervalOfLongitude(numBits);
        double rightLng = lon + getMinIntervalOfLongitude(numBits);

        String leftUp = encode(upLat, leftLng);
        list.add(leftUp);

        String leftMid = encode(lat, leftLng);
        list.add(leftMid);

        String leftDown = encode(downLat, leftLng);
        list.add(leftDown);

        String midUp = encode(upLat, lon);
        list.add(midUp);

        String midMid = encode(lat, lon);
        list.add(midMid);

        String midDown = encode(downLat, lon);
        list.add(midDown);

        String rightUp = encode(upLat, rightLng);
        list.add(rightUp);

        String rightMid = encode(lat, rightLng);
        list.add(rightMid);

        String rightDown = encode(downLat, rightLng);
        list.add(rightDown);

        //Log.i("okunu", "getArroundGeoHash list = " + list.toString());
        return list;
    }


    public static Map<String,NslGpsPoint> calGeoHashStringMap4Region(NslGpsPoint pointA, NslGpsPoint pointB){
        return calGeoHashStringMap4Region(pointA.getLongitude(),pointA.getLatitude(),pointB.getLongitude(),pointB.getLatitude(),defaultBitLength);
    }

    static public Map<String, NslGpsPoint> calGeoHashStringMap4Region(double longitude1, double latitude1, double longitude2, double latitude2){
        return calGeoHashStringMap4Region( longitude1,  latitude1,  longitude2,  latitude2 ,defaultBitLength);
    }

    static public Set<GeoHash> calGeoHashSet4Region(double longitude1, double latitude1, double longitude2, double latitude2) {
        return calGeoHashSet4Region(longitude1,latitude1,longitude2,latitude2,defaultBitLength);
    }


    static public Set<GeoHash> calGeoHashSet4Region(double longitude1, double latitude1, double longitude2, double latitude2,int numBits){
        Map<String, NslGpsPoint> map=calGeoHashStringMap4Region(longitude1,latitude1,longitude2,latitude2,numBits);
        Set<String> geoHashStringSet=map.keySet();
//        log.info("geoHashStringSet.size() ={}",geoHashStringSet.size());
        Collection<NslGpsPoint> pointSet=map.values();
//        log.info("pointSet.size() ={}",pointSet.size());
        Set<GeoHash> geoHashSet=new HashSet<>();
        for (NslGpsPoint point : pointSet) {
            GeoHash geoHash=new GeoHash(point.getLongitude(),point.getLatitude(),numBits);
            geoHashSet.add(geoHash);
        }
        return geoHashSet;

    }


    // 给一个经纬度区域范围,返回区域内所有的<geohashvalue,points>,其中points 为中心点经纬度
    /**
     * 计算某一个区域内的所有 geoHash, 以map形式返回，注意如果区域过大的话，可能会内存溢出
     * 需要控制调用 分割函数，分割较大区域后，调用本函数
     * @param longitude1
     * @param latitude1
     * @param longitude2
     * @param latitude2
     * @return key=geoHash, value=point
     */
    static public Map<String, NslGpsPoint> calGeoHashStringMap4Region(double longitude1, double latitude1, double longitude2, double latitude2 , int numBits){
        Map<String, NslGpsPoint> map=new HashMap<>();

        double maxLongitude=Math.max(longitude1,longitude2);
        double minLongitude=Math.min(longitude1,longitude2);


        double maxLatitude=Math.max(latitude1,latitude2);
        double minLatitude=Math.min(latitude1,latitude2);


        double currentLongitude=minLongitude;
        double currentLatitude=minLatitude;
        double midLongitude=0;
        double midLatitude=0;

        String geoHashValue=null;

        double minLngInterval=getMinIntervalOfLongitude(numBits);
        double minLatInterval=getMinIntervalOfLatitude(numBits);

        int i=0;

        while (currentLatitude<maxLatitude){
            while (currentLongitude<maxLongitude){
               /* if(i==10){
                    break;
                }*/

                geoHashValue=encode(currentLongitude,currentLatitude);
                midLongitude=getMidLongitude(currentLongitude,numBits);
                midLatitude=getMidLatitude(currentLatitude,numBits);

                NslGpsPoint NslGpsPoint=new NslGpsPoint(geoHashValue,midLongitude,midLatitude);

                //System.out.println(NslGpsPoint);
                map.put(geoHashValue,NslGpsPoint);

                currentLongitude+= minLngInterval;

                i++;
//                System.out.println(i);
            }
            //break;
            currentLatitude+=minLatInterval;
            currentLongitude=minLongitude;
        }

        return map;
    }


    /**
     * 对传入的区域，按1k*1k 分割，即相当于把 1k*1k 的方块，放置到 传入的区域内，每次扣出来 1k*1k的大小
     * 栅格大小按照 19m*38m 考虑
     * @param lon1
     * @param lat1
     * @param lon2
     * @param lat2
     * @return
     */
    public static List<NslGpsPoint[]> divideRegion(double lon1,double lat1,double lon2,double lat2,int numBits){
        List<NslGpsPoint[]> pointPairList=new ArrayList<>();

        double maxLongitude=Math.max(lon1,lon2);
        double minLongitude=Math.min(lon1,lon2);

        double maxLatitude=Math.max(lat1,lat2);
        double minLatitude=Math.min(lat1,lat2);


        double intervalLon=getMinIntervalOfLongitude(numBits);
        double intervalLat=getMinIntervalOfLatitude(numBits);
//        log.info("intervalLon = {}",intervalLon);
//        log.info("intervalLat = {}",intervalLat);

        int sideLength=1000;

        double w5IntervalLon=intervalLon*sideLength;
        double w5IntervalLat=intervalLat*sideLength;

        int xCount= (int) (Math.floor((maxLongitude-minLongitude)/w5IntervalLon))+1;
        int yCount= (int) (Math.floor((maxLatitude-minLatitude)/w5IntervalLat))+1;

        double beginLongitude=minLongitude;
        double beginLatitude=minLatitude;

        for (int i = 0; i < xCount; i++) {
            beginLatitude=minLatitude+i*w5IntervalLat;
            for (int j = 0; j < yCount; j++) {
                beginLongitude=minLongitude+j*w5IntervalLon;
                NslGpsPoint[] points=new NslGpsPoint[2];
                points[0]=new NslGpsPoint(i+"_"+j,beginLongitude,beginLatitude);

                double boundaryLon=Math.min(maxLongitude,beginLongitude+w5IntervalLon);
                double boundaryLat=Math.min(maxLatitude,beginLatitude+w5IntervalLat);

                points[1]=new NslGpsPoint((i+1)+"_"+(j+1),boundaryLon,boundaryLat);
                pointPairList.add(points);

            }
        }
        return pointPairList;
    }

    public static List<NslGpsPoint[]> divideRegion(double lon1,double lat1,double lon2,double lat2){
        return divideRegion(lon1,lat1,lon2,lat2,defaultBitLength);
    }

    public static void buildCsvFilesOfGeoHash4Region(String outputFilePath,NslGpsPoint pointA,NslGpsPoint pointB){
        buildCsvFilesOfGeoHash4Region(outputFilePath,pointA.getLongitude(),pointA.getLatitude(),pointB.getLongitude(),pointB.getLatitude());
    }


    /**
     * 将 指定矩形区域内的geoHash写成 多个csv文件；
     * 每个文件 最多包含 5k*5k 个 geoHash 记录
     * 每条 geoHash 记录为：
     *   "geohash,midlon,midlat,rectangle_gps“ : geoHash,中心点经纬度，栅格坐标字符串
     *   其中栅格坐标字符串形式为 “113.658340;34.757690_113.660080;34.758030_113.661170;34.758200"，以方便生成mapinfo 的 mif 文件
     * @param outputFilePath
     * @param lon1
     * @param lat1
     * @param lon2
     * @param lat2
     */
    public static void buildCsvFilesOfGeoHash4Region(String outputFilePath,double lon1,double lat1,double lon2,double lat2){
        List<NslGpsPoint[]> pointPairList=divideRegion(lon1,lat1,lon2,lat2);
//        log.info("pointPairList = {}", NslUtilPrinter.getJsonWithFastJsonPrettyAndNUll(pointPairList));
        int i=0;
        int size=pointPairList.size();
        for (NslGpsPoint[] nslGpsPoints : pointPairList) {
            Map<String,NslGpsPoint> geoHashMap= calGeoHashStringMap4Region(nslGpsPoints[0],nslGpsPoints[1]);
//            log.info("geoHashMap size ={}",geoHashMap.keySet().size());
            String outputFilePathSub= NslUtilFilePath.buildFilePathWithIdTokenAtRear(outputFilePath,"p"+(i+1)+"_"+size);
//            log.info("outputFilePathSub ={}",outputFilePathSub);
            geoHashMap2File(geoHashMap,outputFilePathSub);
            i++;
        }
    }

    /**
     * 将 geoHash Map 写成文件，文件格式为csv，
     * 内容为：
     * "geohash,midlon,midlat,rectangle_gps“ : geoHash,中心点经纬度，栅格坐标字符串
     * 其中栅格坐标字符串形式为 “113.658340;34.757690_113.660080;34.758030_113.661170;34.758200"，以方便生成mapinfo 的 mif 文件
     * @param geoHashMap
     * @param outputFilePath
     */
    public static void geoHashMap2File(Map<String,NslGpsPoint> geoHashMap,String outputFilePath){
        //for order
        List<String> geoHashStringList=new ArrayList<>();

        List<String> lineList=new ArrayList<>();
        String headline="geohash,midlon,midlat,rectangle_gps";
        lineList.add(headline);

        Set<String> geoHashSet=geoHashMap.keySet();
        geoHashStringList.addAll(geoHashSet);

        for (String geoHashString : geoHashStringList) {
            String line="";
            NslGpsPoint pointGis=geoHashMap.get(geoHashString);

            List<NslGpsPoint> pointGisList= getVertexListByLonLat(pointGis.getLongitude(),pointGis.getLatitude());
            String rectangleGpsString="";
            for (NslGpsPoint point : pointGisList) {
                rectangleGpsString+=point.getLongitude()+";"+point.getLatitude()+"_";
            }
            rectangleGpsString= NslUtilString.deleteTail(rectangleGpsString,1);

            line=geoHashString+","+pointGis.getLongitude()+","+pointGis.getLatitude()+","+rectangleGpsString;

            lineList.add(line);
        }

        NslUtilCsv.writeCsvFile(new File(outputFilePath),lineList);
    }

    /**
     * 计算 polygon 匹配的 geoHashCollection
     * @param polygon
     * @param geoHashCollection
     * @return
     */
    public  static Set<GeoHash> calculateGeoHashListOfPolygon(NslPolygon polygon, Collection<GeoHash> geoHashCollection){
        Set<GeoHash> geoHashSet=new HashSet<>();

        for (GeoHash geoHash : geoHashCollection) {
            double lon=geoHash.getLongitude();
            double lat=geoHash.getLatitude();
            Point2D.Double point=new Point2D.Double(lon,lat);
            if(NslUtilGis.isInPolygonByJdk(point,polygon)){
                geoHashSet.add(geoHash);
            }
        }

        return geoHashSet;
    }

    /**
     * 将 Set 中 的每一个geoHash，包裹的矩形，转成 polygon
     * @param geoHashCollection
     * @return
     */
    public static List<NslPolygon> geoHashCollection2PolygonList(Collection<GeoHash> geoHashCollection){
        List<NslPolygon> polygonList=new ArrayList<>();
        for (GeoHash geoHash : geoHashCollection) {
            List<NslGpsPoint> pointGisList= getVertexListByLonLat(geoHash.getLongitude(),geoHash.getLatitude());
            List<Point2D.Double> polygonPointList=new ArrayList<>();
            for (NslGpsPoint nslGpsPoint : pointGisList) {
                Point2D.Double doublePoint=new Point2D.Double();
                doublePoint.setLocation(nslGpsPoint.getLongitude(),nslGpsPoint.getLatitude());
                polygonPointList.add(doublePoint);
            }
            NslPolygon polygon=new NslPolygon(polygonPointList);
            polygon.setId(geoHash.getGeoHashString());
            polygonList.add(polygon);
        }
        return polygonList;
    }

}
